The power of plugins
The alternateRowColors() function that we wrote is a perfect candidate to become a jQuery plugin.
In fact, any operation that we wish to apply to a set of DOM elements
can easily be expressed as a plugin. To accomplish this, we need to
modify our existing function only a little bit:
jQuery.fn.alternateRowColors = function() {
JavaScript sorting, sortingplug-ins$('tbody tr:odd', this)
.removeClass('even').addClass('odd');
$('tbody tr:even', this)
.removeClass('odd').addClass('even');
return this;
};
We have made three important changes to the function.
It is defined as a new property of jQuery.fn rather than as a standalone function. This registers the function as a plugin method.
We use the keyword this as a replacement for our $table parameter. Within a plugin method, this refers to the jQuery object that is being acted upon.
Finally, we return this at the end of the function. Supplying the jQuery object as the return value makes our new method chainable.
With our new plugin defined, we can call $table.alternateRowColors(), a more natural jQuery statement, instead of alternateRowColors($table).
Performance concerns
Our code works, but it is
quite slow. The culprit is the comparator function, which is performing a
fair amount of work. This comparator will be called many times during
the course of a sort, which means that every extra moment it spends on
processing will be magnified.
The actual sort algorithm used by JavaScript is not defined by the standard. It may be a simple sort like a bubble sort (worst case of Θ(n2) in computational complexity terms) or a more sophisticated approach like quick sort (which is Θ(n log n)
on average). It is safe to say, though, that doubling the number of
items in an array will more than double the number of times the
comparator function is called.
The remedy for our slow comparator is to pre-compute the keys for the comparison. We begin with our current, slow sort function:
rows.sort(function(a, b) {
var keyA = $(a).children('td').eq(column).text()
.toUpperCase();
var keyB = $(b).children('td').eq(column).text()
.toUpperCase();
if (keyA < keyB) return -1;
if (keyA > keyB) return 1;
return 0;
});
We can pull out the key computation and do that in a separate loop:
$.each(rows, function(index, row) {
row.sortKey = $(row).children('td').eq(column)
.text().toUpperCase();
});
rows.sort(function(a, b) {
if (a.sortKey < b.sortKey) return -1;
if (a.sortKey > b.sortKey) return 1;
return 0;
});
$.each(rows, function(index, row) {
$table.children('tbody').append(row);
row.sortKey = null;
});
In the new loop, we are doing all of the expensive work and storing the result in a new .sortKey property. This kind of property, attached to a DOM element but not a normal DOM attribute, is called an expando.
This is a convenient place to store the key, since we need one per
table row element. Now, we can examine this attribute within the
comparator function, and our sort is markedly faster.
We set the expando property to null
after we're done with it to clean up after ourselves. This is not
strictly necessary in this case, but is a good habit to establish
because expando properties left lying around can be the cause of memory leaks. For more information, see Appendix C.
Instead of using expando properties, jQuery provides an alternative data storage mechanism we could use. The .data() method sets or retrieves arbitrary information associated with page elements, and the .removeData() method gets rid of any such stored information:
$.each(rows, function(index, row) {
$(row).data('sortKey', $(row).children('td')
.eq(column).text().toUpperCase());
});
rows.sort(function(a, b) {
if ($(a).data('sortKey') < $(b).data('sortKey'))
return -1;
if ($(a).data('sortKey') > $(b).data('sortKey'))
return 1;
return 0;
});
$.each(rows, function(index, row) {
$table.children('tbody').append(row);
$(row).removeData('sortKey');
});
Using .data() instead
of expando properties can, at times, be more convenient, since we are
often working with jQuery objects rather than directly with DOM nodes.
It also avoids potential problems with Internet Explorer memory leaks.
However, for the remainder of this example, we will stick with expando
properties in order to practice switching between operations on DOM
nodes and operations on jQuery objects.